查看原文
其他

反汇编代码还原之优化方式

TkBinary 看雪学苑 2022-07-01

本文为看雪论坛优秀文章

看雪论坛作者ID:TkBinary




  • 目录


  •  一. 优化方式

  •       1.1 前言

  •       1.2 优化方式分类

  •       1.3 常量折叠

  •       1.4 常量传播

  •       1.5 变量去除

  •       1.6 归并优化

  •       1.7 Cpu流水线优化

  •       1.8 数学变换优化

  •       1.9 不可达分支优化

  •        2.0 代码外提优化


  • 二. 去掉优化方式


  • 三. 总结






一. 优化方式


1.1 前言


在我们学习反汇编的时候,很多人都以为反汇编很难。其实不然,学什么都是有技巧的。
 
而想要锻炼我们的反汇编能力,第一就是要进行多练,第二就是熟悉底层原理,第三就是熟悉套路。
 
往后几篇会把C与C++的反汇编形式体现出来,如果写的不对的话请批评指正。


1.2 优化方式分类


汇编中的加法、减法、乘法、除法、取模等等都是有优化方式以及套路的。
 

优化方式分为以下几种:

  • 常量折叠

  • 常量传播

  • 变量去除

  • 归并优化

  • Cpu流水线优化

  • 数学变换

  • 不可达分支优化

  • 代码外提优化


遇到新的优化再说。

那么着重介绍一下上面优化方式所代表的意思。
 
优化的前提是在 Release下切开启O2选项优化速度的前提 Debug版本也会优化但是更多的是方便程序员调试,所以在不影响调试的前提下才会进行优化。


1.3 常量折叠


有以下例子:

int n = 0;int m = 1;printf("%d",7 + 8);printf("%d",n + 6);printf("%d",n + m);

所谓常量折叠就是在编译前所遇到的常量,是可以进行计算的。那么就会优化为一个常量值
 
例如上面的7+8不会产add指令,而是在程序编译后直接成为15(0xF)。
 
VC6.0VS2019分别编译。为什么要两个编译器?目的就是让大家知道这个套路不管是几十年前的6.0还是现如今的Vs2019 都是一样的,可能会有些稍许不同,但是绝不影响你的逆向以及反汇编,而恰巧这才是真正的核心所在,包括gcc编译也是一样。
 
Vc6.0 保留核心

.text:00401000 push 0Fh.text:00401002 push offset aD ; "%d".text:00401007 call _printf.text:0040100C push 6.text:0040100E push offset aD ; "%d".text:00401013 call _printf.text:00401018 push 1.text:0040101A push offset aD ; "%d".text:0040101F call _printf.text:00401024 add esp, 18h

可以看到7+8在反汇编的形势下直接变为了 0xF,也就是10进制的15。
 
Vs2019

.text:00401040 sub_401040 proc near ; CODE XREF: start-8D↓p.text:00401040 push 0Fh.text:00401042 push offset unk_417A8C.text:00401047 call sub_401010.text:0040104C push 6.text:0040104E push offset unk_417A8C.text:00401053 call sub_401010.text:00401058 push 1.text:0040105A push offset unk_417A8C.text:0040105F call sub_401010.text:00401064 add esp, 18h.text:00401067 xor eax, eax.text:00401069 retn.text:00401069 sub_401040 endp

可以看到唯一不同的就是高版本IDA没有识别出sig库,所以调用的都成了 sub_401010 而低版本函数已经认出来了。
 
还有就是文件体积变大了,vc6.0 编译出来28kb 2019编译出来98kb,你品。
 
上面例子足以说明什么是常量折叠,含义就是常量会在编译器给你计算出来。

1.4 常量传播


常量传播也叫做常量扩散,指的就是变量在写入或者读取的时候没有传递内存地址(&)也没有传指针或者引用来修改值的时候就会发生常量传播。
 
大白话讲就是,你没有修改我变量的代码,那么这个变量我就可以认为是常量了。
 
以上面高级代码为例子:

int n = 0;printf("%d",n + 6);

那么进行常量传播之后,n因为没有对其修改,也没有对其进行传地址的操作, 所以编译器就会把它变为常量了。
 
那么上面的代码就会产生如下代码:

int n = 0;printf("%d",0 + 6);

而看到这里想必大家应该明白了,0 + 6 又符合常量折叠,所以代码继续变化:

int n = 0;printf("%d",6);

以上面汇编为例子:

.text:0040100C push 6.text:0040100E push offset aD ; "%d".text:00401013 call _printf

想必大家知道这里为啥是push 6了。这里进行了两次优化,一次是常量传播,一次是常量折叠

1.5 变量去除


变量去除指的就是你程序中定义了变量但是没有对其进行修改,然后进行常量传播,常量折叠一步一步给优化掉。
 
还是以高级代码为例子:

int n = 0;int m = 1;printf("%d",n + m);

程序首先发现 n m 两个变量相加,但是看了一下上面,发现没有对其进行修改。所以代码就会变为如下:

printf("%d",0 + 1);

而0+1符合常量折叠,所以最终代码就变为了:

printf("%d",1);

对应反汇编:

.text:00401018 push 1.text:0040101A push offset aD ; "%d".text:0040101F call _printf

1.6 归并优化


归并优化,如果可以一起优化那么我就一起优化。
 
我们知道printf属于C调用约定,所以需要外平栈。而且他是可变参,通过你push参数个数的不同,外平栈的大小也会相应改变。
 
比如:

.text:00401018 push 1.text:0040101A push offset aD ; "%d".text:0040101F call _printf add esp,8

但是我们在上面的汇编代码中并没有看到 add esp,8 而是直接看到了 add esp,0x18。
 
原因是什么?在你调用printf的时候,而下面你又调用了相同的几个 printf ,printf 都是C调用约定。
 
所以我就一起给你平了。
 
所以代码就有如下:

.text:00401040 sub_401040 proc near ; CODE XREF: start-8D↓p.text:00401040 push 0Fh.text:00401042 push offset unk_417A8C.text:00401047 call sub_401010.text:0040104C push 6.text:0040104E push offset unk_417A8C.text:00401053 call sub_401010.text:00401058 push 1.text:0040105A push offset unk_417A8C.text:0040105F call sub_401010.text:00401064 add esp, 18h.text:00401067 xor eax, eax.text:00401069 retn.text:00401069 sub_401040 endp

一个参数是4个字节,所以累计总共push了6个参数。4 * 6 = 24个字节,所以平栈也需要24个字节。
 
24恰巧是十六进制的 0x18。

1.7 Cpu流水线优化


Cpu流水线优化其实说白了就是打乱指令执行顺序,而不影响原有功能。这个在我们反汇编的时候需要注意,正常的汇编代码都是平平整整顺顺序序,让人一眼看的很舒服,而且反汇编出高级代码也很快。
 
这里以简单的汇编为例子。因为我要写很多代码才会遇到流水线优化,这里我模拟一下。
 
正常的汇编代码指令顺序:

xor eax,eaxxor ebx,ebxxor ecx,ecxmov eax,1add eax,2mov ebx,eaxmov ecx,3

打乱的流水线:

xor eax,eaxmov eax,1xor ecx,ecxadd eax,2mov ecx,3xor ebx,ebxmov ebx,eax

汇编代码就很简单。
 
我们着重看一下打乱的汇编。
 
在没打乱之前代码平平整整,打乱之后发现很多汇编进行了穿插。
 
比如:

mov eax,1xor ecx,ecx

在Cpu执行 mov eax,1的时候,可以直接执行 xor ecx,ecx ,这样的好处是下一行汇编不依赖于上一行汇编。
 
之前的指令是下一行指令依赖于上一行指令,那么Cpu 如果在执行第二行的时候发现你依赖于上一行汇编,那么就会等待。

mov eax,1add eax,2

第一行执行了 mov eax,1 那么第二行又使用了eax.,那么第二行执行的时候就要等待第一行。
 
而打断的好处就是我执行第一行的时候也可以执行第二行而且不会影响你的结果,也可以提升速度。
 
流水线优化需要你细品。
 
当你品完之后再看下打乱的汇编:

xor eax,eaxmov eax,1xor ecx,ecxadd eax,2mov ecx,3xor ebx,ebxmov ebx,eax

是不是发现很顺眼了。那么当你还原的时候完全可以按照自己的意愿来恢复汇编进行还原:

xor ecx,ecxmov ecx,3 xor eax,eaxmov eax,1add eax,2 xor ebx,ebxmov ebx,eax

是不是就很简单了。

1.8  数学变换优化


数学变换优化:如果操作的数是无意义的,那么就会进行优化。

i = 10;b = 11;i = b + 0;i = b - 0;i = b *3;i = b/3;

那么以上高级代码直接进行优化,优化为:

i = b;

1.9 不可达分支优化


不可达分支则是分支永远都不会走,那么也不会产生汇编代码,也没有存在的意义。

a = 10;if (a == 10){ xxxx}else{ xxxxx}

上面已经知道a就是个常量,值就是10。那么会走if块,而 else永远不会走,那么就会将else优化掉。当然实际情况中代码肯定很多,不会像我一样简单的写一个 a = 10 去判断。


2.0 代码外提优化


所谓代码外提一般是在循环的时候进行优化。循环体内部没有产生修改此变量的代码,就会进行优化。

int x = xxx;while(x > y/3){ xxx.... x--; }

循环体内部并没有操作y/3,所以这个值都会放到外面执行。


则会优化为:

t = y / 3;while(x > t){ xxxxx.... x--;}

而 t 变量很可能也会经过上面的几种优化变为了寄存器变量。





二. 去掉优化方式


代码混淆与优化是对立的,所以学习下优化也方便我们更好的"人肉"优化混淆代码。
 
去掉优化的方式说一下。
 
上面所说都是编译器已经识别到了你的程序没有传地址传指针等,所以我们想办法就不让他优化。
 
简单的函数如果你什么也不做也会给你优化掉的。

int n = 10;int m = 0;scanf("%d",&n);scanf("%d",&m);int c = n + m;scanf("%d",&c);printf("%d%d%d",&n,&m,&c)

我们使用了很多scanf以及printf并对其变量取地址,那么编译器就不知道我们会不会修改变量的值了。
 
那么就不会给我们优化了,此时查看汇编代码就能看到真实的最小限度的优化代码了。




三. 总结


上面的几种优化方式,虽然很简单,但是我们也必须要掌握以及了解的。因为后面的反汇编代码还原会有更多的优化。而那些优化在配合上面所说的优化就会让你感觉很难,或者不知道汇编为什么那样做。而你了解了这些在单独看各自的优化就会明白,也会豁然开朗。

 
这些在我博客上已经写过,这里发出来也是为了让新会员重新了解一下,反汇编高手能复习,新人能学习。
 
CPP代码很简单,本次不提交了。



- End -



看雪ID:TkBinary

https://bbs.pediy.com/user-home-723188.htm

  *本文由看雪论坛 TkBinary 原创,转载请注明来自看雪社区。





本文参与了#看雪30天发帖打卡挑战#活动。


发帖见证成长,坚持见证不凡。


不仅可以收获进步,还可赢取物质奖励哦!


想要了解更多活动详情,戳 ↓




推荐文章++++

* Java序列化反序列化源码---Jackson反序列化漏洞源码分析

* PWN学习Use After Free

* 高版本64位Win10(RS2或更高)下枚举消息钩子的一种思路

* X64内核SMAP,SMEP浅析

* Linux Kernel Pwn 学习笔记 (UAF)







公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



求分享

求点赞

求在看


“阅读原文”一起来充电吧!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存